Agentic FX Dev Squad · Domain Owner Training · Case Study
ABCA — Contract Filter on Group Customer Invoices
A complete feature lifecycle walkthrough for Domain Owner trainees: original brief → open questions → spec evolution → acceptance criteria → what shipped. JEC-3620.
22 production files across Core microservice and Web FrontApp.
44 unit tests. All 13 P0 acceptance criteria stamped. 11 P1/P2 deferred to BAU GA with pilot checklist.
13P0 ACs shipped
22Production files
44Unit tests
11P1/P2 deferred
2Spec corrections mid-build
The problem Domain Owner reads this first
When generating a Customer Grouped Invoice (Invoices → Create Customer Grouped), the picker groups every invoiceable item for the chosen Customer regardless of which underlying Customer Contract the item came from. Customers such as ABCA hold multiple active contracts at once (e.g. CCTV, Fire, Maintenance), so the finance team must manually segregate cost-lines — or accept a mixed invoice and remediate downstream.
There is no first-class way to restrict the candidate set to one or more Customer Contracts before grouping, and the Jobs-tab keyword search does not match on PPM Contract Number, so users cannot quickly assemble a per-contract invoice run.
🎓 Domain Owner lens — what to look for in a Problem Statement
A strong problem statement names the persona (Finance Team), the screen (CGroupInvoice/Create), the pain (mixed contracts, manual remediation), and the missing capability (no contract filter, PPM search gap). If any of these are vague or missing when you read a draft spec, send it back — the AI cannot define scope correctly without a grounded problem.
Scope decisions
✓ In ScopeWhat we are building
Customer Contract(s) multi-select filter under Advanced Filters — Jobs tab
Same filter on the Draft Invoices tab with identical behaviour
Options: ContractNumber - CustomerName, each part truncated to 20 chars
Searchable by Contract Number and Contract Description
Lists only Active and Renewed contracts; Suspended/Cancelled/Deleted excluded
Auto-revoke selected contracts not linked to Customer when Customer is chosen
Clearing Customer field preserves selected contract chips
Standard chip UX: cross icon per chip, Reset Filter clears all
Export honours the active filter (exported rows match visible rows)
Extend Search Jobs to match by PPM Contract Number
Update Search Jobs placeholder to advertise PPM Contract No.
Filter hidden entirely for users lacking Customer Contract permission
✕ Out of ScopeExplicitly excluded
All Jobs / All Invoices listing pages — only the CGroup picker
Grouping, approval, or line-edit logic after items are picked
Out-of-scope decisions are yours alone. The AI Engineer flags technical implications but does not decide. Here: the Draft Invoices tab placeholder was originally in scope — a Domain Owner decision resolved it as out-of-scope because the placeholder already advertised PPM Contract No. See resolution #2 below.
What changed from original brief Core training content
The initial backlog item described the goal in two sentences. When product-domain-expert drafted the spec, it surfaced 8 open questions — areas where the original brief was ambiguous and multiple valid designs existed. A Domain Owner resolved all 8 on 2026-05-19. These decisions are embedded in the final spec. This section shows the original ambiguity vs. the decision made — and why each decision matters to the build.
#1
Which contract statuses should appear in the dropdown?
Original ambiguity
Multiple valid options: show All statuses, or show Active only, or show Active + Renewed, or show Active + Renewed + Suspended. Suspended contracts can still be on jobs (BR-303).
Decision made
Active and Renewed only. Suspended and Cancelled are excluded. The filter predicate is !IsSuspended && !IsCancelled (Deleted rows excluded upstream per BR-1112). This keeps the picker clean — users pick live contracts only.
#2
Should the Draft Invoices tab Search placeholder also be updated?
Original ambiguity
Backlog item mentioned both tabs needing PPM Contract No. search visibility. Was the Draft Invoices tab also missing PPM discovery?
Decision made
No change to Draft Invoices tab placeholder. Existing placeholder already reads Site / Address / Account No. / Order No. / PPM Contract No. — PPM is already advertised. Only the Jobs tab placeholder needs updating.
#3
When the user clears the Customer field, what happens to selected contract chips?
Original ambiguity
Two valid behaviours: (A) clear Customer → revoke contracts linked to that customer. (B) clear Customer → preserve all selected chips. Each has UX trade-offs.
Decision made
Preserve chips when Customer is cleared. Only selecting a Customer triggers auto-revoke of mismatched contracts. Clearing Customer simply re-broadens the dropdown to all tenant contracts. This avoids surprise data loss.
#4
With no Customer selected — what contracts are listed and in what format?
Original ambiguity
Should it show all tenant contracts or only contracts for recently-used customers? And how do labels look when multiple customers appear together?
Decision made
All Active/Renewed tenant contracts. Label format: ContractNumber - CustomerName, each part truncated to 20 characters when longer. This ensures the picker is usable even when browsing cross-customer contracts.
#5
What does "PPM Contract Number match" mean on the Jobs tab?
Original ambiguity
PPM Contract Number could match: (A) the PPM contract that spawned the job, or (B) any PPM contract linked to the job's site. Different JOIN paths in the query.
Decision made
PPM identity — the PPM contract that spawned the job, same semantics as the existing Draft Invoices tab match. This keeps both tabs consistent and avoids broad site-level false positives.
#6
Should the filter be behind a per-tenant feature flag for the ABCA pilot?
Original ambiguity
Standard approach for pilots: add a feature flag so only ABCA sees the feature. This lets quick rollback without a code deploy. But adds complexity to maintain.
Decision made
Straight code deploy — no feature flag. Pilot rollout is gated at deploy/release level (ABCA staging environment), not via runtime config. Rollback is a revert deploy, not a toggle flip. Simpler code; accepted risk at this pilot scale.
#7
Does the existing Export action on the picker need to honour the new filter?
Original ambiguity
The Export button already existed. Was it in scope to verify/ensure it respected the new Customer Contract(s) filter, or was it assumed to "just work"?
Decision made
Export must honour the new filter. Explicitly confirmed as in-scope to verify — the exported file must contain only the rows visible under the active filter. This matched existing Joblogic grid-export behaviour (AC-13).
#8
What happens for users who lack the Customer Contract permission (BR-1040)?
Original ambiguity
Options: (A) show filter but disable it with a tooltip, or (B) hide the filter entirely and skip the options API call. Both are valid UX patterns.
Decision made
Hidden entirely — no API call issued. Users without the permission do not see the filter and the client makes zero requests for the options list. This is both cleaner UX and avoids the security risk of calling an endpoint for data the user can't access (NFR-2, AC-21).
Spec corrections discovered during build Implementation revealed two gaps
Two acceptance criteria were corrected mid-feature (Phase 5 code review) because the initial spec wording diverged from what was actually implemented. This is a normal and expected part of the process — but the Domain Owner must sign off on the corrected wording, not just the original.
Lists only Active and Renewed Customer Contracts; Suspended, Cancelled, and Deleted contracts are excluded.
AC-3: "contracts linked to that Customer that are Active or Renewed"
Corrected AC text (ships this)
Filter predicate:!IsSuspended && !IsCancelled
The implementation enforces this in CustomerController.GetCustomers. Deleted rows are excluded upstream per BR-1112. The API endpoint uses: IncludeCompleted=false, IncludeCancelled=false, ExcludeSuspended=true, IsForDropdown=true.
Why this matters: "Active and Renewed" and !IsSuspended && !IsCancelled have the same intent but different technical expression. The code review surfaced the exact predicate so the spec now matches the implementation precisely.
The Vue component's searchHaystack function matched against:
ContractNumber + ContractDescription + CustomerName
This was broader than the spec stated — CustomerName was included unintentionally.
Corrected implementation (what shipped)
Trimmed to exactly:ContractNumber + ContractDescription
AC-6 reads: "filters by Contract Number and Contract Description." CustomerName was removed from searchHaystack in both cGroupInvoiceSearch.js and cGroupDraftInvoiceSearch.js so the search behaviour exactly matches the AC text.
Domain Owner note: This shows why AC text must be precise. "Contract Number and Contract Description" is unambiguous. "Contract details" would have been too loose.
Acceptance criteria Domain Owner signs off on P0 manually
🎓 Domain Owner reminder — how to read ACs
Each AC is a checkable, observable statement. P0 = must-ship for ABCA pilot. P1 = required for BAU GA. P2 = polish. As Domain Owner, you are responsible for walking every P0 AC in the running application — not running scripts, not spot-checking, not delegating. Items marked ↑ corrected were updated mid-build.
Given the user is on the Jobs tab of Create Customer Grouped Invoice, then a Customer Contract(s) filter is rendered under Advanced Filters as a searchable multi-select dropdown. Each option label is formatted Contract Number - Customer Name, with each part truncated to 20 characters when longer.
AC-2
P0
Given the user is on the Draft Invoices tab of Create Customer Grouped Draft Invoice, then the same Customer Contract(s) filter is rendered under Advanced Filters with identical behaviour to AC-1.
AC-3 ↑ corrected
P0
Given a Customer is selected, when the user opens the Customer Contract(s) dropdown, then only contracts linked to that Customer that are not Suspended and not Cancelled are listed (predicate: !IsSuspended && !IsCancelled; Deleted rows excluded upstream per BR-1112).
Corrected in Phase 5 from "Active or Renewed" to the exact predicate implemented in CustomerController.GetCustomers.
AC-4 ↑ corrected
P0
Given no Customer is selected, when the user opens the dropdown, then every Customer Contract in the tenant that is not Suspended and not Cancelled is listed (Deleted excluded per BR-1112). Endpoint flags: IncludeCompleted=false, IncludeCancelled=false, ExcludeSuspended=true, IsForDropdown=true.
Corrected alongside AC-3 to match implementation predicate exactly.
AC-5
P0
Given the user has selected one or more Customer Contracts before selecting a Customer, when the user then selects a Customer, then any previously selected contracts not linked to that Customer are automatically revoked (removed from chip list), and remaining selections persist.
AC-5a
P0
Given a Customer is currently selected and one or more Customer Contracts are selected, when the user clears the Customer field, then the selected Customer Contract chips are preserved (not revoked), and the dropdown's option list re-broadens to all Active/Renewed contracts in the tenant.
AC-6 ↑ corrected
P0
Given the user opens the dropdown and types into the search box, then the dropdown filters by Contract Number and Contract Description (case-insensitive substring match). CustomerName is not a search field.
Corrected in Phase 6: original implementation included CustomerName in searchHaystack. Trimmed to ContractNumber + ContractDescription only to match AC text exactly.
AC-7
P0
Given one or more Customer Contracts are selected and the user runs the candidate search, then the result grid returns only Jobs (Jobs tab) or Draft Invoices (Draft Invoices tab) linked to the selected contract(s); items not linked to any selected contract are excluded.
AC-8
P0
Given a filter combination returns zero rows, then the result area shows the standard empty-state message "No matching results found".
AC-9
P1
Given one or more contract chips are displayed, when the user clicks the cross icon on a chip, then that contract is removed from the selection and the result grid re-queries. (Deferred to BAU GA — pilot checklist TC-010.)
AC-10
P0
Given a Customer Contract(s) selection is active, when the user clicks Reset Filter, then the Customer Contract(s) selection is cleared alongside every other filter (per BR-1252 canonical Reset Filter behaviour).
AC-11
P1
Given the user switches between Jobs and Draft Invoices tabs without reloading, then the Customer Contract(s) selection is retained on the originating tab. (Deferred — pilot checklist TC-012.)
AC-12
P1
Given the user reloads the page (browser refresh), then the Customer Contract(s) selection is cleared (matches Joblogic default reload behaviour). (Deferred — pilot checklist TC-012.)
AC-13
P1
Given a Customer Contract(s) filter is applied and the user triggers the Export action, then the exported file contains only the rows visible under the active filter. (Deferred — pilot checklist TC-013.)
AC-21
P0
Given the signed-in user lacks the Customer Contract permission (BR-1040), then the Customer Contract(s) filter is not rendered on either tab and the client makes no request to fetch the contract options list.
Requirement 2 — Search by PPM Contract No. on the Jobs tab7 ACs
AC-14
P0
Given the user is on the Jobs tab, then the Search Jobs input's placeholder reads exactly: Site / Contact / Description / Order No. / Reference / PPM Contract No.
AC-15
P0
Given the user enters a PPM Contract Number into Search Jobs and submits, then the result grid returns Jobs linked to that PPM contract identity (the PPM that spawned the job — same semantics as Draft Invoices tab) in addition to existing fields (Site, Contact, Description, Order No., Reference).
AC-16
P1
Given a PPM Contract Number search returns zero rows, then the result grid shows "No matching jobs found". (Deferred — pilot checklist TC-016.)
AC-17
P1
Given the Search Jobs input has a value, when the user clicks the cross / clear icon, then the search term is cleared and the grid re-queries without the keyword constraint. (Deferred — pilot checklist TC-016.)
AC-18
P0
Given a Search Jobs keyword is active, when the user clicks Reset Filter, then the keyword is cleared along with all other filters.
AC-19
P1
Given the user switches tabs without reloading, then the Search Jobs keyword is retained on return; on page reload the keyword is cleared. (Deferred — pilot checklist.)
AC-20
P2
Given a PPM-Contract-No. keyword search is active and Export is triggered, then the exported file contains only the returned Jobs. (P2 polish — deferred to backlog.)
Non-functional requirements4 NFRs
NFR-1
P1
Performance: The Customer Contract(s) dropdown loads its option list and the result grid re-queries within Joblogic's existing Advanced-Filter responsiveness budget (target parity with sibling filters such as Job Type / Priority).
NFR-2
P0
Security / permissions: Users without contract visibility do not see the filter at all and no options-list API call is issued. Confirmed by AC-21 wiring in both Vue components and endpoint check.
NFR-3
P1
Accessibility: The new dropdown meets WCAG 2.1 AA — keyboard reachable, chips removable via keyboard, search input has an accessible label, and the placeholder change does not replace a programmatic label. (Deferred — pilot checklist TC-022.)
NFR-4
P1
Audit / regressions: No regression to BR-111 (tax-rate consistency), BR-303 (suspended-contract approval block), BR-304 (frozen-draft exclusion), or BR-1650 (tab eligibility). (Deferred — pilot checklist TC-019/TC-020.)
Key business rules referenced Domain Owner must know these
BR-1040
Customer Contracts feature / permission set. New filter visibility gates on this permission. Users without it see nothing and get no API call.
AC-21, NFR-2 — the security gate.
BR-1041
Contract Number is unique per customer. This is the basis for using Contract Number as the visible label in the dropdown without ambiguity.
AC-1, AC-2 — label format.
BR-1048
Deleting a Customer Contract cascades job/PPM removal. Deleted contracts must not appear in the new dropdown — excluded upstream.
AC-3, AC-4 — exclusion logic.
BR-1112
Deleted Customer Contracts are hidden everywhere; Renewed contracts are searchable. The filter obeys this — Renewed are included (not deleted = not excluded).
AC-3, AC-4 — status inclusion.
BR-1252
Canonical Reset Filter behaviour. All Advanced Filter fields clear together. The new Customer Contract(s) field follows this exactly.
AC-10, AC-18 — Reset Filter.
BR-1650
CGroup Job-tab / Draft-tab eligibility rules. The new filter narrows but does not override eligibility. Only un-invoiced jobs with cost value qualify.
NFR-4 — regression guard.
BR-282
Customer field is mandatory on the CGroup-Draft picker. This spec deliberately allows Contract selection before Customer — with auto-pruning on Customer selection (AC-5).
AC-5, AC-5a — interaction order.
BR-1601
Customer-Contract site-unassign preserves historical linkage. Filter must surface jobs even where the contract's site link has been unassigned. Match on stored Customer Contract ID, not derived site join.
AC-7 — backend query design.
BR-111
CGroup grouping constrained by consistent tax rates across cost lines. The new filter narrows candidates but must not bypass this invariant. Existing rule continues to govern.
NFR-4 — regression guard.
Domain Owner lessons from this feature
Spec Drafting
Open questions are a sign of a healthy spec process
This feature had 8 open questions — all legitimate design decisions the AI correctly flagged rather than assumed.
Your job as Domain Owner is to resolve these with product authority, not to guess or delegate back.
Each resolution here had a real downstream impact: the "no feature flag" decision changed deployment planning; the "preserve chips on clear" decision changed Vue component logic.
If a spec has zero open questions, read it more critically — the AI may have made assumptions silently.
AC Review
Precise AC text prevents implementation divergence
AC-6 initially said "filters by Contract Number and Contract Description." The build included CustomerName in the search — a superset that only Phase 6 re-check caught.
Had the AC said "Contract details" this would have been invisible and shipped incorrectly.
Every noun in an AC is load-bearing. "Contract Number and Contract Description" is a list — treat it as exhaustive, not illustrative.
AC-3/AC-4 correction shows that even correct intent can be expressed in implementer-unfriendly language. Match the AC to the predicate the code will execute.
Scope Decisions
Out-of-scope is as important as in-scope
The Draft Invoices tab placeholder started as in scope in the original brief. Product review revealed it was already handled — making it explicitly out of scope saved build time.
"No per-tenant feature flag" was a scope decision that simplified the codebase. Document the rationale — future developers need to know this was deliberate, not an oversight.
Out-of-scope decisions should be written into the spec as explicit exclusions — not just left unmentioned. If it was discussed and excluded, it must be documented.
Ship Gate
P0/P1/P2 staging lets pilots ship without blocking on polish
13 P0 ACs shipped; 11 P1/P2 items deferred with GWT cases as the pilot checklist for ABCA.
The Domain Owner's ship gate is: all P0 ACs checked in the running application. Not all ACs. Not "most" ACs. P0s only for the pilot.
Items deferred to BAU GA still have written test cases — they are not forgotten, they are tracked. This is the difference between deferral and abandonment.
Approving "all P0 ACs ✓" is a binding sign-off. You own the business correctness of what ABCA receives on 2026-05-20.